package com.ccx.demo.business.common.service;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.ccx.demo.business.common.vo.FileInfo;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.multipart.MultipartFile;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.SeekableByteChannel;
import java.util.List;
import java.util.Optional;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * 文件读写服务接口
 *
 * @author 谢长春 2020-11-13
 */
@Validated
public interface IFileService {

    /**
     * uname 生成规则
     */
    enum UnameType {
        MD5, UUID
    }

    /**
     * 单文件上传，文件已存在时不再重复时不上传
     * <pre>
     * // 以 uuid 生成文件名，每次生成的文件名不一样
     * write(
     *   uploadFile,
     *   FileInfo.valueOf(FPath.FileName
     *       .of(file.getOriginalFilename(), "level-1", "level-2")
     *       .buildUuidUname()
     *   )
     * );
     * // 以文件内容 MD5 构造文件名，如果文件内容无变化，则每次 md5 值一样，表示重复文件
     * write(
     *   uploadFile,
     *   FileInfo.valueOf(FPath.FileName
     *       .of(file.getOriginalFilename(), "level-1", "level-2")
     *       .buildMd5Uname(uploadFile.getInputStream())
     *   )
     * );
     * </pre>
     *
     * @param uploadFile MultipartFile 上传文件
     * @param fileInfo   <T extends FileInfo> 存储对象
     * @return FileInfo
     */
    default @NotNull(message = "返回值不能为null") <T extends FileInfo> T write(
            @NotNull(message = "参数【uploadFile】不能为null") final MultipartFile uploadFile,
            @NotNull(message = "参数【fileInfo】不能为null") final T fileInfo
    ) {
        return write(uploadFile, fileInfo, false);
    }

    /**
     * 单文件上传，文件重复时，可选择替换内容
     * <pre>
     * // 以 uuid 生成文件名，每次生成的文件名不一样
     * write(
     *   uploadFile,
     *   FileInfo.valueOf(FPath.FileName
     *       .of(file.getOriginalFilename(), "level-1", "level-2")
     *       .buildUuidUname()
     *   ),
     *   true
     * );
     * // 以文件内容 MD5 构造文件名，如果文件内容无变化，则每次 md5 值一样，表示重复文件
     * write(
     *   uploadFile,
     *   FileInfo.valueOf(FPath.FileName
     *       .of(file.getOriginalFilename(), "level-1", "level-2")
     *       .buildMd5Uname(uploadFile.getInputStream())
     *   ),
     *   true
     * );
     * </pre>
     *
     * @param uploadFile MultipartFile 上传文件
     * @param fileInfo   <T extends FileInfo> 存储对象
     * @param replace    boolean 是否替换已存在的文件， true：是， false：否
     * @return FileInfo
     */
    @NotNull(message = "返回值不能为null") <T extends FileInfo> T write(
            @NotNull(message = "参数【uploadFile】不能为null") final MultipartFile uploadFile,
            @NotNull(message = "参数【fileInfo】不能为null") final T fileInfo,
            final boolean replace
    );

    /**
     * 写入内容，文件已存在时不再重复写入
     * <pre>
     * // 以 uuid 生成文件名，每次生成的文件名不一样
     * String content = "text content";
     * write(
     *   content,
     *   FileInfo.valueOf(FPath.FileName
     *       .of("文本内容.txt", "level-1", "level-2")
     *       .buildUuidUname()
     *   )
     * );
     * // 以文件内容 MD5 构造文件名，如果文件内容无变化，则每次 md5 值一样，表示重复文件
     * String content = "text content";
     * write(
     *   content,
     *   FileInfo.valueOf(FPath.FileName
     *       .of("文本内容.txt", "level-1", "level-2")
     *       .buildMd5Uname(content)
     *   )
     * );
     * </pre>
     *
     * @param content  String 写入内容
     * @param fileInfo <T extends FileInfo> 存储对象
     * @return FileInfo
     */
    default @NotNull(message = "返回值不能为null") <T extends FileInfo> T write(
            @NotEmpty(message = "参数【content】不能为空") final String content,
            @NotNull(message = "参数【fileInfo】不能为null") final T fileInfo
    ) {
        return write(content.getBytes(UTF_8), fileInfo, false);
    }

    /**
     * 写入内容，文件已存在时可选择覆盖文件
     * <pre>
     * // 以 uuid 生成文件名，每次生成的文件名不一样
     * String content = "text content";
     * write(
     *   content,
     *   FileInfo.valueOf(FPath.FileName
     *       .of("文本内容.txt", "level-1", "level-2")
     *       .buildUuidUname()
     *   ),
     *   true
     * );
     * // 以文件内容 MD5 构造文件名，如果文件内容无变化，则每次 md5 值一样，表示重复文件
     * String content = "text content";
     * write(
     *   content,
     *   FileInfo.valueOf(FPath.FileName
     *       .of("文本内容.txt", "level-1", "level-2")
     *       .buildMd5Uname(content)
     *   ),
     *   true
     * );
     * </pre>
     *
     * @param content  String 写入内容
     * @param fileInfo <T extends FileInfo> 存储对象
     * @param replace  boolean 是否替换已存在的文件， true：是， false：否
     * @return FileInfo
     */
    default @NotNull(message = "返回值不能为null") <T extends FileInfo> T write(
            @NotEmpty(message = "参数【content】不能为空") final String content,
            @NotNull(message = "参数【fileInfo】不能为null") final T fileInfo,
            final boolean replace
    ) {
        return write(content.getBytes(UTF_8), fileInfo, replace);
    }

    /**
     * 写入内容，文件已存在时不会覆盖文件
     * <pre>
     * // 以 uuid 生成文件名，每次生成的文件名不一样
     * byte[] bytes = "二进制内容".getBytes(UTF_8);
     * write(
     *   bytes,
     *   FileInfo.valueOf(FPath.FileName
     *       .of("二进制内容.txt", "level-1", "level-2")
     *       .buildUuidUname()
     *   )
     * );
     * // 以文件内容 MD5 构造文件名，如果文件内容无变化，则每次 md5 值一样，表示重复文件
     * byte[] bytes = "二进制内容".getBytes(UTF_8);
     * write(
     *   bytes,
     *   FileInfo.valueOf(FPath.FileName
     *       .of("二进制内容.txt", "level-1", "level-2")
     *       .buildMd5Uname(bytes)
     *   )
     * );
     * </pre>
     *
     * @param bytes    byte[] 写入内容
     * @param fileInfo <T extends FileInfo> 存储对象
     * @return FileInfo
     */
    default @NotNull(message = "返回值不能为null") <T extends FileInfo> T write(
            @NotEmpty(message = "参数【bytes】不能为空") final byte[] bytes,
            @NotNull(message = "参数【fileInfo】不能为null") final T fileInfo
    ) {
        return write(bytes, fileInfo, false);
    }

    /**
     * 写入内容，文件已存在时可选择覆盖文件
     * <pre>
     * // 以 uuid 生成文件名，每次生成的文件名不一样
     * byte[] bytes = "二进制内容".getBytes(UTF_8);
     * write(
     *   bytes,
     *   FileInfo.valueOf(FPath.FileName
     *       .of("二进制内容.txt", "level-1", "level-2")
     *       .buildUuidUname()
     *   ),
     *   true
     * );
     * // 以文件内容 MD5 构造文件名，如果文件内容无变化，则每次 md5 值一样，表示重复文件
     * byte[] bytes = "二进制内容".getBytes(UTF_8);
     * write(
     *   bytes,
     *   FileInfo.valueOf(FPath.FileName
     *       .of("二进制内容.txt", "level-1", "level-2")
     *       .buildMd5Uname(bytes)
     *   ),
     *   true
     * );
     * </pre>
     *
     * @param bytes    byte[] 写入内容
     * @param fileInfo <T extends FileInfo> 存储对象
     * @param replace  boolean 是否替换已存在的文件， true：是， false：否
     * @return FileInfo
     */
    @NotNull(message = "返回值不能为null") <T extends FileInfo> T write(
            @NotEmpty(message = "参数【bytes】不能为空") final byte[] bytes,
            @NotNull(message = "参数【fileInfo】不能为null") final T fileInfo,
            final boolean replace
    );

    /**
     * 写入内容，文件已存在时不会覆盖文件
     * <pre>
     * // 以 uuid 生成文件名，每次生成的文件名不一样
     * File file = new File("/tmp/nginx.conf");
     * write(
     *   file,
     *   FileInfo.valueOf(FPath.FileName
     *       .of(file.getName(), "level-1", "level-2")
     *       .buildUuidUname()
     *   )
     * );
     * // 以文件内容 MD5 构造文件名，如果文件内容无变化，则每次 md5 值一样，表示重复文件
     * File file = new File("/tmp/nginx.conf");
     * write(
     *   file,
     *   FileInfo.valueOf(FPath.FileName
     *       .of(file.getName(), "level-1", "level-2")
     *       .buildMd5Uname(file)
     *   )
     * );
     * </pre>
     *
     * @param file     File 原始文件
     * @param fileInfo <T extends FileInfo> 存储对象
     * @return FileInfo
     */
    default @NotNull(message = "返回值不能为null") <T extends FileInfo> T write(
            @NotNull(message = "参数【file】不能为null") final File file,
            @NotNull(message = "参数【fileInfo】不能为null") final T fileInfo
    ) {
        return write(file, fileInfo, false);
    }

    /**
     * 写入内容，文件已存在时可选择覆盖文件
     * <pre>
     * // 以 uuid 生成文件名，每次生成的文件名不一样
     * File file = new File("/tmp/nginx.conf");
     * write(
     *   file,
     *   FileInfo.valueOf(FPath.FileName
     *       .of(file.getName(), "level-1", "level-2")
     *       .buildUuidUname()
     *   ),
     *   true
     * );
     * // 以文件内容 MD5 构造文件名，如果文件内容无变化，则每次 md5 值一样，表示重复文件
     * File file = new File("/tmp/nginx.conf");
     * write(
     *   file,
     *   FileInfo.valueOf(FPath.FileName
     *       .of(file.getName(), "level-1", "level-2")
     *       .buildMd5Uname(file)
     *   ),
     *   true
     * );
     * </pre>
     *
     * @param file     File 原始文件
     * @param fileInfo <T extends FileInfo> 存储对象
     * @param replace  boolean 是否替换已存在的文件， true：是， false：否
     * @return FileInfo
     */
    @NotNull(message = "返回值不能为null") <T extends FileInfo> T write(
            @NotNull(message = "参数【file】不能为null") final File file,
            @NotNull(message = "参数【fileInfo】不能为null") final T fileInfo,
            final boolean replace
    );

//    /**
//     * 写入内容，文件已存在时不会覆盖文件
//     * <pre>
//     * // 以 uuid 生成文件名，每次生成的文件名不一样
//     * \@Cleanup final FileInputStream fileInputStream = new FileInputStream(new File("/path/file.txt"));
//     * \@Cleanup final BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
//     * write(
//     *   bufferedInputStream,
//     *   FileInfo.valueOf(FPath.FileName
//     *       .of("file.txt", "level-1", "level-2")
//     *       .buildUuidUname()
//     *   )
//     * );
//     * // 以文件内容 MD5 构造文件名，如果文件内容无变化，则每次 md5 值一样，表示重复文件
//     * \@Cleanup final FileInputStream fileInputStream = new FileInputStream(new File("/path/file.txt"));
//     * \@Cleanup final BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
//     * write(
//     *   bufferedInputStream,
//     *   FileInfo.valueOf(FPath.FileName
//     *       .of("file.txt", "level-1", "level-2")
//     *       .buildMd5Uname(bufferedInputStream)
//     *   )
//     * );
//     * </pre>
//     *
//     * @param bufferedInputStream BufferedInputStream 原始数据流
//     * @param fileInfo            <T extends FileInfo> 存储对象
//     * @return FileInfo
//     */
//    default @NotNull(message = "返回值不能为null") <T extends FileInfo> T write(
//            @NotNull(message = "参数【stream】不能为null") final BufferedInputStream bufferedInputStream,
//            @NotNull(message = "参数【fileInfo】不能为null") final T fileInfo
//    ) {
//        return write(bufferedInputStream, fileInfo, false);
//    }
//
//    /**
//     * 写入内容，文件已存在时可选择覆盖文件
//     * <pre>
//     * // 以 uuid 生成文件名，每次生成的文件名不一样
//     * \@Cleanup final FileInputStream fileInputStream = new FileInputStream(new File("/path/file.txt"));
//     * \@Cleanup final BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
//     * write(
//     *   bufferedInputStream,
//     *   FileInfo.valueOf(FPath.FileName
//     *       .of("file.txt", "level-1", "level-2")
//     *       .buildUuidUname()
//     *   ),
//     *   true
//     * );
//     * // 以文件内容 MD5 构造文件名，如果文件内容无变化，则每次 md5 值一样，表示重复文件
//     * \@Cleanup final FileInputStream fileInputStream = new FileInputStream(new File("/path/file.txt"));
//     * \@Cleanup final BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
//     * write(
//     *   bufferedInputStream,
//     *   FileInfo.valueOf(FPath.FileName
//     *       .of("file.txt", "level-1", "level-2")
//     *       .buildMd5Uname(bufferedInputStream)
//     *   ),
//     *   true
//     * );
//     * </pre>
//     *
//     * @param bufferedInputStream BufferedInputStream 原始数据流
//     * @param fileInfo            <T extends FileInfo> 存储对象
//     * @param replace             boolean 是否替换已存在的文件， true：是， false：否
//     * @return FileInfo
//     */
//    @NotNull(message = "返回值不能为null") <T extends FileInfo> T write(
//            @NotNull(message = "参数【stream】不能为null") final BufferedInputStream bufferedInputStream,
//            @NotNull(message = "参数【fileInfo】不能为null") final T fileInfo,
//            final boolean replace
//    );

    /**
     * 写入内容，将对象 JSON.toJSONString(object) 之后写入文件，文件已存在时不会覆盖文件
     * <pre>
     * write(
     *   "{\"content\":\"json 文件内容\"}",
     *   FileInfo.valueOf(FPath.FileName.of("result.json", "level-1", "level-2") )
     * );
     * </pre>
     *
     * @param object   Object 写入对象
     * @param fileInfo <T extends FileInfo> 存储对象
     * @return FileInfo
     */
    default @NotNull(message = "返回值不能为null") <T extends FileInfo> T writeJson(
            final Object object,
            @NotNull(message = "参数【fileInfo】不能为null") final T fileInfo
    ) {
        return writeJson(object, fileInfo, false);
    }

    /**
     * 写入内容，将对象 JSON.toJSONString(object) 之后写入文件，文件已存在时可选择覆盖文件
     * <pre>
     * // **********
     * write(
     *   "{\"content\":\"json 文件内容\"}",
     *   FileInfo.valueOf(FPath.FileName.of("result.json", "level-1", "level-2") )
     *   true
     * );
     * // **********
     * write(
     *   new Result(),
     *   FileInfo.valueOf(FPath.FileName.of("result.json", "level-1", "level-2") )
     *   true
     * );
     * </pre>
     *
     * @param object   Object 写入对象
     * @param fileInfo <T extends FileInfo> 存储对象
     * @param replace  boolean 是否替换已存在的文件， true：是， false：否
     * @return FileInfo
     */
    default @NotNull(message = "返回值不能为null") <T extends FileInfo> T writeJson(
            final Object object,
            @NotNull(message = "参数【fileInfo】不能为null") final T fileInfo,
            final boolean replace
    ) {
        return write(JSON.toJSONString(object), fileInfo, replace);
    }

    /**
     * 读取内容为文本
     *
     * @param file <T extends FileInfo> 存储对象
     * @return Optional<String>
     */
    <T extends FileInfo> Optional<String> readString(@NotNull(message = "参数【file】不能为null") final T file);

    /**
     * 读取内容转换为 JSON， 自定义转换规则
     *
     * @param file <T extends FileInfo> 存储对象
     * @return Optional<String>
     */
    <T extends FileInfo, R> Optional<R> readJson(
            @NotNull(message = "参数【file】不能为null") final T file,
            @NotNull(message = "参数【typeReference】不能为null") final TypeReference<R> typeReference
    );

    /**
     * 读取内容转换为 JSON 对象
     *
     * @param file <T extends FileInfo> 存储对象
     * @return Optional<R>
     */
    <T extends FileInfo, R> Optional<R> readJsonObject(
            @NotNull(message = "参数【file】不能为null") final T file,
            @NotNull(message = "参数【clazz】不能为null") final Class<R> clazz
    );

    /**
     * 读取内容转换为 JSON 数组
     *
     * @param file <T extends FileInfo> 存储对象
     * @return Optional<List < R>>
     */
    <T extends FileInfo, R> Optional<List<R>> readJsonArray(
            @NotNull(message = "参数【file】不能为null") final T file,
            @NotNull(message = "参数【clazz】不能为null") final Class<R> clazz
    );

    /**
     * 读取文件二进制数组
     *
     * @param file <T extends FileInfo> 存储对象
     * @return byte[]
     */
    <T extends FileInfo> byte[] readBytes(@NotNull(message = "参数【file】不能为null") final T file);

    /**
     * 读取文件流：字符流
     *
     * @param file <T extends FileInfo> 存储对象
     * @return BufferedReader
     */
    <T extends FileInfo> BufferedReader readStream(@NotNull(message = "参数【file】不能为null") final T file);

    /**
     * 读取文件流：字节流
     *
     * @param file <T extends FileInfo> 存储对象
     * @return InputStream
     */
    <T extends FileInfo> InputStream readInputStream(@NotNull(message = "参数【file】不能为null") final T file);

    /**
     * 读取文件流：Channel
     *
     * @param file <T extends FileInfo> 存储对象
     * @return SeekableByteChannel
     */
    <T extends FileInfo> SeekableByteChannel readChannel(@NotNull(message = "参数【file】不能为null") final T file);

    /**
     * 复制内容到目标文件
     *
     * @param file         <T extends FileInfo> 存储对象
     * @param absolutePath String 目标文件绝对路径
     */
    <T extends FileInfo> void copy(@NotNull(message = "参数【file】不能为null") final T file, @NotNull final String absolutePath);

    /**
     * 复制内容输出到 OutputStream
     *
     * @param file         <T extends FileInfo> 存储对象
     * @param outputStream OutputStream 输出目标
     */
    <T extends FileInfo> void copy(@NotNull(message = "参数【file】不能为null") final T file, @NotNull final OutputStream outputStream);
}
